/*!
 * jQuery Mobile Popup @VERSION
 * http://jquerymobile.com
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

//>>label: Popups
//>>group: Widgets
//>>description: Popup windows
//>>docs: http://api.jquerymobile.com/popup/
//>>demos: http://demos.jquerymobile.com/@VERSION/popup/
//>>css.structure: ../css/structure/jquery.mobile.popup.css
//>>css.structure: ../css/structure/jquery.mobile.transition.css
//>>css.structure: ../css/structure/jquery.mobile.transition.fade.css
//>>css.theme: ../css/themes/default/jquery.mobile.theme.css

// Lessons:
// You must remove nav bindings even if there is no history. Make sure you
// remove nav bindings in the same frame as the beginning of the close process
// if there is no history. If there is history, remove nav bindings from the nav
// bindings handler - that way, only one of them can fire per close process.

( function( factory ) {
	if ( typeof define === "function" && define.amd ) {

		// AMD. Register as an anonymous module.
		define( [
			"jquery",
			"jquery-ui/focusable",
			"jquery-ui/safe-active-element",
			"jquery-ui/safe-blur",
			"../widget",
			"./widget.theme",
			"../support",
			"../events/navigate",
			"../navigation/path",
			"../navigation/history",
			"../navigation/navigator",
			"../navigation/method",
			"../animationComplete",
			"../navigation" ], factory );
	} else {

		// Browser globals
		factory( jQuery );
	}
} )( function( $ ) {

function pointInRectangle( x, y, windowCoordinates ) {
	return ( x >= windowCoordinates.x && x <= windowCoordinates.x + windowCoordinates.cx &&
		y >= windowCoordinates.y && y <= windowCoordinates.y + windowCoordinates.cy );
}

function isOutOfSight( element, windowCoordinates ) {
	var offset = element.offset(),
		width = element.outerWidth( true ),
		height = element.outerHeight( true );

	return !(
		pointInRectangle( offset.left, offset.top, windowCoordinates ) ||
		pointInRectangle( offset.left + width, offset.top, windowCoordinates ) ||
		pointInRectangle( offset.left + width, offset.top + height, windowCoordinates ) ||
		pointInRectangle( offset.left, offset.top + height, windowCoordinates ) );
}

function fitSegmentInsideSegment( windowSize, segmentSize, offset, desired ) {
	var returnValue = desired;

	if ( windowSize < segmentSize ) {

		// Center segment if it's bigger than the window
		returnValue = offset + ( windowSize - segmentSize ) / 2;
	} else {

		// Else center it at the desired coordinate while keeping it completely inside the window
		returnValue = Math.min( Math.max( offset, desired - segmentSize / 2 ),
			offset + windowSize - segmentSize );
	}

	return returnValue;
}

function getWindowCoordinates( theWindow ) {
	return {
		x: theWindow.scrollLeft(),
		y: theWindow.scrollTop(),
		cx: ( theWindow[ 0 ].innerWidth || theWindow.width() ),
		cy: ( theWindow[ 0 ].innerHeight || theWindow.height() )
	};
}

var hook = function() {

	// Links within content areas, tests included with page
	$( this )
		.find( "a" )
		.jqmEnhanceable()
		.filter( ":jqmData(rel='popup')[href][href!='']" )
		.each( function() {

			// Accessibility info for popups
			var element = this,
				idref = element.getAttribute( "href" ).substring( 1 );

			if ( idref ) {
				element.setAttribute( "aria-haspopup", true );
				element.setAttribute( "aria-owns", idref );
				element.setAttribute( "aria-expanded", false );
			}
		} );
};

( $.enhance = $.extend( $.enhance, $.extend( { hooks: [] }, $.enhance ) ) ).hooks.push( hook );

$.widget( "mobile.popup", {
	version: "@VERSION",

	options: {
		classes: {
			"ui-popup": "ui-corner-all ui-overlay-shadow"
		},
		theme: "inherit",
		overlayTheme: "inherit",
		transition: "none",
		positionTo: "origin",
		tolerance: null,
		closeLinkSelector: "a[data-ui-rel='back']",
		dismissible: true,
		enhanced: false,

		// NOTE Windows Phone 7 - 8.1 has a scroll position caching issue that requires us to
		//      disable popup history management by default
		//      https://github.com/jquery/jquery-mobile/issues/4784
		//
		// NOTE this option is modified in _create!
		history: !( $.mobile.browser.oldIE || $.mobile.browser.newIEMobile )
	},

	// When the user depresses the mouse/finger on an element inside the popup while the popup is
	// open, we ignore resize events for a short while. This prevents #6961.
	_handleDocumentVmousedown: function( theEvent ) {
		if ( this._isOpen && $.contains( this._ui.container[ 0 ], theEvent.target ) ) {
			this._ignoreResizeEvents();
		}
	},

	_create: function() {
		var theElement = this.element,
			myId = theElement.attr( "id" ),
			currentOptions = this.options;

		// We need to adjust the history option to be false if there's no AJAX nav. We can't do it
		// in the option declarations because those are run before it is determined whether there
		// shall be AJAX nav.
		currentOptions.history = currentOptions.history && $.mobile.ajaxEnabled &&
			$.mobile.hashListeningEnabled;

		// Define instance variables
		$.extend( this, {
			_scrollTop: 0,
			_page: theElement.closest( ".ui-page" ),
			_ui: null,
			_fallbackTransition: "",
			_currentTransition: false,
			_prerequisites: null,
			_isOpen: false,
			_tolerance: null,
			_resizeData: null,
			_ignoreResizeTo: 0,
			_orientationchangeInProgress: false
		} );

		if ( this._page.length === 0 ) {
			this._page = $( "body" );
		}

		if ( currentOptions.enhanced ) {
			this._ui = {
				container: theElement.parent(),
				screen: theElement.parent().prev(),
				placeholder: $( this.document[ 0 ].getElementById( myId + "-placeholder" ) )
			};

			// We call _addClasses() even though as per the requirements for enhanced: true, the
			// classes should already be added, because we want tracking for the classes. This
			// should not result in reflows, because the class attribute will be set to a value it
			// already has.
			this._addClasses();
		} else {
			this._enhance();
			this._applyTransition( currentOptions.transition );
		}
		this
			._setTolerance( currentOptions.tolerance )
			._ui.focusElement = this._ui.container;

		// Event handlers
		this._on( this._ui.screen, { "vclick": "_eatEventAndClose" } );
		this._on( this.window, {
			orientationchange: "_handleWindowOrientationchange",
			resize: "_handleWindowResize",
			keyup: "_handleWindowKeyUp"
		} );
		this._on( this.document, {
			vmousedown: "_handleDocumentVmousedown",
			focusin: "_handleDocumentFocusIn"
		} );
	},

	_themeElements: function() {
		return [
			{
				element: this._ui.screen,
				prefix: "ui-overlay-",
				option: "overlayTheme"
			},
			{
				element: this.element,
				prefix: "ui-body-"
			}
		];
	},

	_addClasses: function() {
		this._addClass( this._ui.placeholder, null, "ui-screen-hidden" );
		this._addClass( this._ui.screen, "ui-popup-screen", "ui-screen-hidden" );
		this._addClass( this._ui.container,
			"ui-popup-container ui-popup-hidden ui-popup-truncate" );
		this._addClass( "ui-popup" );
	},

	_enhance: function() {
		var myId = this.element.attr( "id" ),
			placeholder = "placeholder",
			ui = {
				screen: $( "<div>" ),
				placeholder: $( "<div>" ),
				container: $( "<div>" )
			},
			fragment = this.document[ 0 ].createDocumentFragment();

		this._ui = ui;

		fragment.appendChild( ui.screen[ 0 ] );
		fragment.appendChild( ui.container[ 0 ] );

		if ( myId ) {
			ui.screen.attr( "id", myId + "-screen" );
			ui.container.attr( "id", myId + "-popup" );
			ui.placeholder.attr( "id", myId + "-placeholder" );
			placeholder = "placeholder for " + myId;
		}

		// Using a wonderful golfing tradition we leave a token where the element used to be so we
		// know where to put it back upon _destroy()
		ui.placeholder.html( "<!-- " + placeholder + " -->" ).insertBefore( this.element );

		// We detach the payload, add the classes, and then insert it into the popup container
		this.element.detach();

		// This._ui needs to be fully established before we can all this._addClasses(), because
		// this._addClasses() uses this._ui, but we don't want any of the UI elements to be
		// attached, so as to avoid reflows as this._addClasses() adds classes.
		this._addClasses();

		// Apply the proto
		this.element.appendTo( ui.container );
		this._page[ 0 ].appendChild( fragment );

		return ui;
	},

	_eatEventAndClose: function( theEvent ) {
		theEvent.preventDefault();
		theEvent.stopImmediatePropagation();
		if ( this.options.dismissible ) {
			this.close();
		}
		return false;
	},

	// Make sure the screen covers the entire document - CSS is sometimes not enough for
	// accomplishing this.
	_resizeScreen: function() {
		var screen = this._ui.screen,
			popupHeight = this._ui.container.outerHeight( true ),
			screenHeight = screen.removeAttr( "style" ).height(),

			// Subtracting 1 here is necessary for an obscure Android 4.0 bug where the browser
			// hangs if the screen covers the entire document :/
			documentHeight = this.document.height() - 1;

		if ( screenHeight < documentHeight ) {
			screen.height( documentHeight );
		} else if ( popupHeight > screenHeight ) {
			screen.height( popupHeight );
		}
	},

	_handleWindowKeyUp: function( theEvent ) {
		if ( this._isOpen && theEvent.keyCode === $.mobile.keyCode.ESCAPE ) {
			return this._eatEventAndClose( theEvent );
		}
	},

	_expectResizeEvent: function() {
		var windowCoordinates = getWindowCoordinates( this.window );

		if ( this._resizeData ) {
			if ( windowCoordinates.x === this._resizeData.windowCoordinates.x &&
					windowCoordinates.y === this._resizeData.windowCoordinates.y &&
					windowCoordinates.cx === this._resizeData.windowCoordinates.cx &&
					windowCoordinates.cy === this._resizeData.windowCoordinates.cy ) {

				// Timeout not refreshed
				return false;
			} else {

				// Clear existing timeout - it will be refreshed below
				clearTimeout( this._resizeData.timeoutId );
			}
		}

		this._resizeData = {
			timeoutId: this._delay( "_resizeTimeout", 200 ),
			windowCoordinates: windowCoordinates
		};

		return true;
	},

	_resizeTimeout: function() {
		if ( this._isOpen ) {
			if ( !this._expectResizeEvent() ) {
				if ( this._ui.container.hasClass( "ui-popup-hidden" ) ) {

					// Effectively rapid-open the popup while leaving the screen intact
					this._removeClass( this._ui.container, "ui-popup-hidden ui-popup-truncate" );
					this.reposition( { positionTo: "window" } );
					this._ignoreResizeEvents();
				}

				this._resizeScreen();
				this._resizeData = null;
				this._orientationchangeInProgress = false;
			}
		} else {
			this._resizeData = null;
			this._orientationchangeInProgress = false;
		}
	},

	_stopIgnoringResizeEvents: function() {
		this._ignoreResizeTo = 0;
	},

	_ignoreResizeEvents: function() {
		if ( this._ignoreResizeTo ) {
			clearTimeout( this._ignoreResizeTo );
		}
		this._ignoreResizeTo = this._delay( "_stopIgnoringResizeEvents", 1000 );
	},

	_handleWindowResize: function( /* theEvent */ ) {
		if ( this._isOpen && this._ignoreResizeTo === 0 ) {
			if ( isOutOfSight( this._ui.container, getWindowCoordinates( this.window ) ) &&
					( this._expectResizeEvent() || this._orientationchangeInProgress ) &&
					!this._ui.container.hasClass( "ui-popup-hidden" ) ) {

				// Effectively rapid-close the popup while leaving the screen intact
				this._addClass( this._ui.container, "ui-popup-hidden ui-popup-truncate" );
				this._ui.container.removeAttr( "style" );
			}
		}
	},

	_handleWindowOrientationchange: function( /* theEvent */ ) {
		if ( !this._orientationchangeInProgress && this._isOpen && this._ignoreResizeTo === 0 ) {
			this._expectResizeEvent();
			this._orientationchangeInProgress = true;
		}
	},

	// When the popup is open, attempting to focus on an element that is not a child of the popup
	// will redirect focus to the popup
	_handleDocumentFocusIn: function( theEvent ) {
		var target,
			targetElement = theEvent.target,
			ui = this._ui;

		if ( !this._isOpen ) {
			return;
		}

		if ( targetElement !== ui.container[ 0 ] ) {
			target = $( targetElement );
			if ( !$.contains( ui.container[ 0 ], targetElement ) ) {
				$( $.ui.safeActiveElement( this.document[ 0 ] ) ).one( "focus",
					$.proxy( function() {
						$.ui.safeBlur( targetElement );
					}, this ) );
				ui.focusElement.focus();
				theEvent.preventDefault();
				theEvent.stopImmediatePropagation();
				return false;
			} else if ( ui.focusElement[ 0 ] === ui.container[ 0 ] ) {
				ui.focusElement = target;
			}
		}

		this._ignoreResizeEvents();
	},

	_applyTransition: function( value ) {
		if ( value ) {
			this._removeClass( this._ui.container, null, this._fallbackTransition );
			if ( value !== "none" ) {
				this._fallbackTransition = $.mobile._maybeDegradeTransition( value );
				if ( this._fallbackTransition === "none" ) {
					this._fallbackTransition = "";
				}
				this._addClass( this._ui.container, null, this._fallbackTransition );
			}
		}

		return this;
	},

	_setOptions: function( newOptions ) {
		if ( newOptions.transition !== undefined ) {
			if ( !this._currentTransition ) {
				this._applyTransition( newOptions.transition );
			}
		}

		if ( newOptions.tolerance !== undefined ) {
			this._setTolerance( newOptions.tolerance );
		}

		if ( newOptions.disabled !== undefined ) {
			if ( newOptions.disabled ) {
				this.close();
			}
		}

		return this._super( newOptions );
	},

	_setTolerance: function( value ) {
		var tol = { t: 30, r: 15, b: 30, l: 15 },
			ar;

		if ( value !== undefined ) {
			ar = String( value ).split( "," );

			$.each( ar, function( idx, val ) {
				ar[ idx ] = parseInt( val, 10 );
			} );

			switch ( ar.length ) {

			// All values are to be the same
			case 1:
				if ( !isNaN( ar[ 0 ] ) ) {
					tol.t = tol.r = tol.b = tol.l = ar[ 0 ];
				}
				break;

			// The first value denotes top/bottom tolerance, and the second value denotes
			// left/right tolerance
			case 2:
				if ( !isNaN( ar[ 0 ] ) ) {
					tol.t = tol.b = ar[ 0 ];
				}
				if ( !isNaN( ar[ 1 ] ) ) {
					tol.l = tol.r = ar[ 1 ];
				}
				break;

			// The array contains values in the order top, right, bottom, left
			case 4:
				if ( !isNaN( ar[ 0 ] ) ) {
					tol.t = ar[ 0 ];
				}
				if ( !isNaN( ar[ 1 ] ) ) {
					tol.r = ar[ 1 ];
				}
				if ( !isNaN( ar[ 2 ] ) ) {
					tol.b = ar[ 2 ];
				}
				if ( !isNaN( ar[ 3 ] ) ) {
					tol.l = ar[ 3 ];
				}
				break;

			default:
				break;
			}
		}

		this._tolerance = tol;
		return this;
	},

	_clampPopupWidth: function( infoOnly ) {
		var menuSize,
			windowCoordinates = getWindowCoordinates( this.window ),

			// Rectangle within which the popup must fit
			rectangle = {
				x: this._tolerance.l,
				y: windowCoordinates.y + this._tolerance.t,
				cx: windowCoordinates.cx - this._tolerance.l - this._tolerance.r,
				cy: windowCoordinates.cy - this._tolerance.t - this._tolerance.b
			};

		if ( !infoOnly ) {

			// Clamp the width of the menu before grabbing its size
			this._ui.container.css( "max-width", rectangle.cx );
		}

		menuSize = {
			cx: this._ui.container.outerWidth( true ),
			cy: this._ui.container.outerHeight( true )
		};

		return { rc: rectangle, menuSize: menuSize };
	},

	_calculateFinalLocation: function( desired, clampInfo ) {
		var returnValue,
			rectangle = clampInfo.rc,
			menuSize = clampInfo.menuSize;

		// Center the menu over the desired coordinates, while not going outside the window
		// tolerances. This will center wrt. the window if the popup is too large.
		returnValue = {
			left: fitSegmentInsideSegment( rectangle.cx, menuSize.cx, rectangle.x, desired.x ),
			top: fitSegmentInsideSegment( rectangle.cy, menuSize.cy, rectangle.y, desired.y )
		};

		// Make sure the top of the menu is visible
		returnValue.top = Math.max( 0, returnValue.top );

		// If the height of the menu is smaller than the height of the document align the bottom
		// with the bottom of the document

		returnValue.top -= Math.min( returnValue.top,
			Math.max( 0, returnValue.top + menuSize.cy - this.document.height() ) );

		return returnValue;
	},

	// Try and center the overlay over the given coordinates
	_placementCoords: function( desired ) {
		return this._calculateFinalLocation( desired, this._clampPopupWidth() );
	},

	_createPrerequisites: function( screenPrerequisite, containerPrerequisite, whenDone ) {
		var prerequisites,
			self = this;

		// It is important to maintain both the local variable prerequisites and
		// self._prerequisites. The local variable remains in the closure of the functions which
		// call the callbacks passed in. The comparison between the local variable and
		// self._prerequisites is necessary, because once a function has been passed to
		// .animationComplete() it will be called next time an animation completes, even if that's
		// not the animation whose end the function was supposed to catch (for example, if an abort
		// happens during the opening animation, the .animationComplete handler is not called for
		// that animation anymore, but the handler remains attached, so it is called the next time
		// the popup is opened - making it stale. Comparing the local variable prerequisites to the
		// widget-level variable self._prerequisites ensures that callbacks triggered by a stale
		// .animationComplete will be ignored.

		prerequisites = {
			screen: $.Deferred(),
			container: $.Deferred()
		};

		prerequisites.screen.done( function() {
			if ( prerequisites === self._prerequisites ) {
				screenPrerequisite();
			}
		} );

		prerequisites.container.done( function() {
			if ( prerequisites === self._prerequisites ) {
				containerPrerequisite();
			}
		} );

		$.when( prerequisites.screen, prerequisites.container ).done( function() {
			if ( prerequisites === self._prerequisites ) {
				self._prerequisites = null;
				whenDone();
			}
		} );

		self._prerequisites = prerequisites;
	},

	_animate: function( args ) {

		// NOTE Before removing the default animation of the screen this had an animate callback
		//      that would resolve the deferred. Now the deferred is resolved immediately
		// TODO Remove the dependency on the screen deferred.
		this._removeClass( this._ui.screen, null, args.classToRemove )
			._addClass( this._ui.screen, null, args.screenClassToAdd );

		args.prerequisites.screen.resolve();

		if ( args.transition && args.transition !== "none" ) {
			if ( args.applyTransition ) {
				this._applyTransition( args.transition );
			}
			if ( this._fallbackTransition ) {
				this._addClass( this._ui.container, null, args.containerClassToAdd )
					._removeClass( this._ui.container, null, args.classToRemove );
				this._ui.container
					.animationComplete( $.proxy( args.prerequisites.container, "resolve" ) );
				return;
			}
		}
		this._removeClass( this._ui.container, null, args.classToRemove );
		args.prerequisites.container.resolve();
	},

	// The desired coordinates passed in will be returned untouched if no reference element can be
	// identified via desiredPosition.positionTo. Nevertheless, this function ensures that its
	// return value always contains valid x and y coordinates by specifying the center middle of
	// the window if the coordinates are absent. options: {
	//   x: coordinate,
	//   y: coordinate,
	//   positionTo: string: "origin", "window", or jQuery selector
	// }
	_desiredCoords: function( openOptions ) {
		var offset,
			dst = null,
			windowCoordinates = getWindowCoordinates( this.window ),
			x = openOptions.x,
			y = openOptions.y,
			pTo = openOptions.positionTo;

		// Establish which element will serve as the reference
		if ( pTo && pTo !== "origin" ) {
			if ( pTo === "window" ) {
				x = windowCoordinates.cx / 2 + windowCoordinates.x;
				y = windowCoordinates.cy / 2 + windowCoordinates.y;
			} else {
				try {
					dst = $( pTo );
				} catch ( err ) {
					dst = null;
				}
				if ( dst ) {
					dst.filter( ":visible" );
					if ( dst.length === 0 ) {
						dst = null;
					}
				}
			}
		}

		// If an element was found, center over it
		if ( dst ) {
			offset = dst.offset();
			x = offset.left + dst.outerWidth() / 2;
			y = offset.top + dst.outerHeight() / 2;
		}

		// Make sure x and y are valid numbers - center over the window
		if ( $.type( x ) !== "number" || isNaN( x ) ) {
			x = windowCoordinates.cx / 2 + windowCoordinates.x;
		}
		if ( $.type( y ) !== "number" || isNaN( y ) ) {
			y = windowCoordinates.cy / 2 + windowCoordinates.y;
		}

		return { x: x, y: y };
	},

	_reposition: function( openOptions ) {

		// We only care about position-related parameters for repositioning
		openOptions = {
			x: openOptions.x,
			y: openOptions.y,
			positionTo: openOptions.positionTo
		};
		this._trigger( "beforeposition", undefined, openOptions );
		this._ui.container.offset( this._placementCoords( this._desiredCoords( openOptions ) ) );
	},

	reposition: function( openOptions ) {
		if ( this._isOpen ) {
			this._reposition( openOptions );
		}
	},

	_openPrerequisitesComplete: function() {
		var id = this.element.attr( "id" ),
			firstFocus = this._ui.container.find( ":focusable" ).first(),
			focusElement = $.ui.safeActiveElement( this.document[ 0 ] );

		this._addClass( this._ui.container, "ui-popup-active" );
		this._isOpen = true;
		this._resizeScreen();

		// Check to see if currElement is not a child of the container.  If it's not, blur
		if ( focusElement && !$.contains( this._ui.container[ 0 ], focusElement ) ) {
			$.ui.safeBlur( focusElement );
		}
		if ( firstFocus.length > 0 ) {
			this._ui.focusElement = firstFocus;
		}
		this._ignoreResizeEvents();
		if ( id ) {
			this.document.find( "[aria-haspopup='true'][aria-owns='" +
				$.mobile.path.hashToSelector( id ) + "']" ).attr( "aria-expanded", true );
		}
		this._ui.container.attr( "tabindex", 0 );
		this._trigger( "afteropen" );
	},

	_open: function( options ) {
		var openOptions = $.extend( {}, this.options, options ),

			// TODO move blacklist to private method
			androidBlacklist = ( function() {
				var ua = navigator.userAgent,

					// Rendering engine is Webkit, and capture major version
					wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ),
					wkversion = !!wkmatch && wkmatch[ 1 ],
					androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ),
					andversion = !!androidmatch && androidmatch[ 1 ],
					chromematch = ua.indexOf( "Chrome" ) > -1;

				// Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and
				// not Chrome.
				if ( androidmatch !== null && andversion === "4.0" && wkversion &&
						wkversion > 534.13 && !chromematch ) {
					return true;
				}
				return false;
			}() );

		// Count down to triggering "popupafteropen" - we have two prerequisites:
		// 1. The popup window animation completes (container())
		// 2. The screen opacity animation completes (screen())
		this._createPrerequisites(
			$.noop,
			$.noop,
			$.proxy( this, "_openPrerequisitesComplete" ) );

		this._currentTransition = openOptions.transition;
		this._applyTransition( openOptions.transition );

		this._removeClass( this._ui.screen, null, "ui-screen-hidden" );
		this._removeClass( this._ui.container, "ui-popup-truncate" );

		// Give applications a chance to modify the contents of the container before it appears
		this._reposition( openOptions );

		this._removeClass( this._ui.container, "ui-popup-hidden" );

		if ( this.options.overlayTheme && androidBlacklist ) {

			// TODO: The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an
			// issue where the popup overlay appears to be z-indexed above the popup itself when
			// certain other styles exist on the same page -- namely, any element set to
			// `position: fixed` and certain types of input. These issues are reminiscent of
			// previously uncovered bugs in older versions of Android's native browser:
			// https://github.com/scottjehl/Device-Bugs/issues/3
			// This fix closes the following bugs ( I use "closes" with reluctance, and stress that
			// this issue should be revisited as soon as possible ):
			// https://github.com/jquery/jquery-mobile/issues/4816
			// https://github.com/jquery/jquery-mobile/issues/4844
			// https://github.com/jquery/jquery-mobile/issues/4874

			// TODO sort out why this._page isn't working
			this._addClass( this.element.closest( ".ui-page" ), "ui-popup-open" );
		}
		this._animate( {
			additionalCondition: true,
			transition: openOptions.transition,
			classToRemove: "",
			screenClassToAdd: "in",
			containerClassToAdd: "in",
			applyTransition: false,
			prerequisites: this._prerequisites
		} );
	},

	_closePrerequisiteScreen: function() {
		this._removeClass( this._ui.screen, null, "out" )
			._addClass( this._ui.screen, null, "ui-screen-hidden" );
	},

	_closePrerequisiteContainer: function() {
		this._removeClass( this._ui.container, null, "reverse out" )
			._addClass( this._ui.container, "ui-popup-hidden ui-popup-truncate" );
		this._ui.container.removeAttr( "style" );
	},

	_closePrerequisitesDone: function() {
		var container = this._ui.container,
			id = this.element.attr( "id" );

		// Remove the global mutex for popups
		$.mobile.popup.active = undefined;

		// Blur elements inside the container, including the container
		$( ":focus", container[ 0 ] ).add( container[ 0 ] ).blur();

		if ( id ) {
			this.document.find( "[aria-haspopup='true'][aria-owns='" +
				$.mobile.path.hashToSelector( id ) + "']" ).attr( "aria-expanded", false );
		}

		this._ui.container.removeAttr( "tabindex" );

		// Alert users that the popup is closed
		this._trigger( "afterclose" );
	},

	_close: function( immediate ) {
		this._removeClass( this._ui.container, "ui-popup-active" )
			._removeClass( this._page, "ui-popup-open" );

		this._isOpen = false;

		// Count down to triggering "popupafterclose" - we have two prerequisites:
		// 1. The popup window reverse animation completes (container())
		// 2. The screen opacity animation completes (screen())
		this._createPrerequisites(
			$.proxy( this, "_closePrerequisiteScreen" ),
			$.proxy( this, "_closePrerequisiteContainer" ),
			$.proxy( this, "_closePrerequisitesDone" ) );

		this._animate( {
			additionalCondition: this._ui.screen.hasClass( "in" ),
			transition: ( immediate ? "none" : ( this._currentTransition ) ),
			classToRemove: "in",
			screenClassToAdd: "out",
			containerClassToAdd: "reverse out",
			applyTransition: true,
			prerequisites: this._prerequisites
		} );
	},

	_unenhance: function() {
		if ( this.options.enhanced ) {
			return;
		}

		this.element

			// Cannot directly insertAfter() - we need to detach() first, because
			// insertAfter() will do nothing if the payload div was not attached
			// to the DOM at the time the widget was created, and so the payload
			// will remain inside the container even after we call insertAfter().
			// If that happens and we remove the container a few lines below, we
			// will cause an infinite recursion - #5244
			.detach()
			.insertAfter( this._ui.placeholder );
		this._ui.screen.remove();
		this._ui.container.remove();
		this._ui.placeholder.remove();
	},

	_destroy: function() {
		if ( this.options.enhanced ) {
			this.classesElementLookup = {};
		}
		if ( $.mobile.popup.active === this ) {
			this.element.one( "popupafterclose", $.proxy( this, "_unenhance" ) );
			this.close();
		} else {
			this._unenhance();
		}
	},

	_closePopup: function( theEvent, data ) {
		var parsedDst, toUrl,
			immediate = false;

		if ( $.mobile.popup.active !== this ||
				( theEvent &&
					( theEvent.isDefaultPrevented() ||
					( theEvent.type === "navigate" && data && data.state && data.state.url &&
						data.state.url === this._myUrl ) ) ) ||
				!this._isOpen ) {
			return;
		}

		// Restore location on screen
		window.scrollTo( 0, this._scrollTop );

		if ( theEvent && theEvent.type === "pagebeforechange" && data ) {

			// Determine whether we need to rapid-close the popup, or whether we can take the time
			// to run the closing transition
			if ( typeof data.toPage === "string" ) {
				parsedDst = data.toPage;
			} else {
				parsedDst = data.toPage.jqmData( "url" );
			}
			parsedDst = $.mobile.path.parseUrl( parsedDst );
			toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash;

			if ( this._pageUrl !== $.mobile.path.makeUrlAbsolute( toUrl ) ||
					data.options.reloadPage ) {

				// Going to a different page - close immediately
				immediate = true;
			} else {
				theEvent.preventDefault();
			}
		}

		// Remove nav bindings
		this._off( this.window, "navigate pagebeforechange" );

		// Unbind click handlers added when history is disabled
		this._off( this.element, "click" );

		this._close( immediate );
	},

	// Any navigation event after a popup is opened should close the popup.
	// NOTE The pagebeforechange is bound to catch navigation events that don't alter the url
	//      (eg, dialogs from popups)
	_bindContainerClose: function() {
		this._on( true, this.window, {
			navigate: "_closePopup",
			pagebeforechange: "_closePopup"
		} );
	},

	widget: function() {
		return this._ui.container;
	},

	_handleCloseLink: function( theEvent ) {
		this.close();
		theEvent.preventDefault();
	},

	// TODO No clear deliniation of what should be here and what should be in _open. Seems to be
	// "visual" vs "history" for now
	open: function( options ) {
		var url, hashkey, activePage, currentIsDialog, hasHash, urlHistory,
			events = {},
			self = this,
			currentOptions = this.options;

		// Make sure open is idempotent
		if ( $.mobile.popup.active || currentOptions.disabled ) {
			return this;
		}

		// Set the global popup mutex
		$.mobile.popup.active = this;
		this._scrollTop = this.window.scrollTop();

		// If history alteration is disabled close on navigate events and leave the url as is
		if ( !( currentOptions.history ) ) {
			self._open( options );
			self._bindContainerClose();

			// When history is disabled we have to grab the data-rel back link clicks so we can
			// close the popup instead of relying on history to do it for us
			events[ "click " + currentOptions.closeLinkSelector ] = "_handleCloseLink";
			this._on( events );

			return this;
		}

		// Cache some values for min/readability
		urlHistory = $.mobile.navigate.history;
		hashkey = $.mobile.dialogHashKey;
		activePage = $.mobile.activePage;
		currentIsDialog = ( activePage ? activePage.hasClass( "ui-page-dialog" ) : false );
		this._pageUrl = url = urlHistory.getActive().url;
		hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog &&
			( urlHistory.activeIndex > 0 );

		if ( hasHash ) {
			self._open( options );
			self._bindContainerClose();
			return this;
		}

		// If the current url has no dialog hash key proceed as normal otherwise, if the page is a
		// dialog simply tack on the hash key
		if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ) {
			url = url + ( url.indexOf( "#" ) > -1 ? hashkey : "#" + hashkey );
		} else {
			url = $.mobile.path.parseLocation().hash + hashkey;
		}

		// Swallow the the initial navigation event, and bind for the next
		this.window.one( "beforenavigate", function( theEvent ) {
			theEvent.preventDefault();
			self._open( options );
			self._bindContainerClose();
		} );

		this.urlAltered = true;
		this._myUrl = url;
		$.mobile.navigate( url, { role: "dialog" } );

		return this;
	},

	close: function() {

		// Make sure close is idempotent
		if ( $.mobile.popup.active !== this ) {
			return this;
		}

		this._scrollTop = this.window.scrollTop();

		if ( this.options.history && this.urlAltered ) {
			$.mobile.back();
			this.urlAltered = false;
		} else {

			// Simulate the nav bindings having fired
			this._closePopup();
		}

		return this;
	}
} );

$.widget( "mobile.popup", $.mobile.popup, $.mobile.widget.theme );

// TODO this can be moved inside the widget
$.mobile.popup.handleLink = function( $link ) {
	var offset,
		path = $.mobile.path,

		// NOTE make sure to get only the hash from the href because ie7 (wp7)
		//      returns the absolute href in this case ruining the element selection
		popup = $( path.hashToSelector( path.parseUrl( $link.attr( "href" ) ).hash ) ).first();

	if ( popup.length > 0 && popup.data( "mobile-popup" ) ) {
		offset = $link.offset();
		popup.popup( "open", {
			x: offset.left + $link.outerWidth() / 2,
			y: offset.top + $link.outerHeight() / 2,
			transition: $link.jqmData( "transition" ),
			positionTo: $link.jqmData( "position-to" )
		} );
	}

	// Remove after delay
	setTimeout( function() {
		$link.removeClass( "ui-button-active" );
	}, 300 );
};

// TODO move inside _create
$.mobile.document.on( "pagebeforechange", function( theEvent, data ) {
	if ( data.options.role === "popup" ) {
		$.mobile.popup.handleLink( data.options.link );
		theEvent.preventDefault();
	}
} );

return $.mobile.popup;

} );
